namespace ScrumSprintMonitor.Samples.Feed
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.IO;
	using System.Text;
	using System.Web;

	using ContinuousLinq.Reactive;

	using Extensibility;

	using Implementation;

	using Infrastructure;

	using Properties;

	using RSS;

	#endregion

	public class FeedBuilderService : ReactiveObject,
	                                  IDisposable
	{
		#region Private Fields

		private readonly ILogger<FeedBuilderService> logger;
		private readonly string outputPath;
		private readonly string rssFeedFilePath;
		private readonly IServiceProvider serviceProvider;
		private IReportableData reportableData;
		private Rss rssFeed;
		private ITeamMemberInfoProvider teamMemberInfoProvider;
		private IWorkItemObserver workItemObserver;

		#endregion

		#region Construction

		static FeedBuilderService()
		{
			var dependsOn = Register<FeedBuilderService>();

			dependsOn.Call(me => me.SubscribeWorkItems()).OnChanged(me => me.ReportableData.Connection.IsConnected).OnChanged(
				me => me.ReportableData.CurrentSprint.WorkItemStore);
		}

		public FeedBuilderService(IAddInHost<FeedAddIn> addInHost, IReportableData reportableData, ITeamMemberInfoProvider teamMemberInfoProvider,
		                          ILogger<FeedBuilderService> logger, IServiceProvider serviceProvider)
		{
			if (reportableData == null)
			{
				throw new ArgumentNullException("reportableData");
			}
			if (teamMemberInfoProvider == null)
			{
				throw new ArgumentNullException("teamMemberInfoProvider");
			}
			if (logger == null)
			{
				throw new ArgumentNullException("logger");
			}
			if (serviceProvider == null)
			{
				throw new ArgumentNullException("serviceProvider");
			}
			if (addInHost == null)
			{
				throw new ArgumentNullException("addInHost");
			}

			// Try to use the target folder specified in the config file
			this.outputPath = Settings.Default.TargetRssFeedFolderPath;
			if (string.IsNullOrEmpty(this.outputPath))
			{
				// If nothing is specified, use the default output folder for this addin
				this.outputPath = addInHost.GetOutputDirectory();
			}

			this.teamMemberInfoProvider = teamMemberInfoProvider;
			this.logger = logger;
			this.serviceProvider = serviceProvider;

			this.rssFeedFilePath = Path.Combine(this.outputPath, "sprint-activity.xml");

			if (File.Exists(this.rssFeedFilePath))
			{
				this.rssFeed = Rss.Load(this.rssFeedFilePath);
			}
			else
			{
				this.rssFeed = new Rss("0.91")
				               	{
				               		channel =
				               			{
				               				title = "Activity Feed",
				               				image = new Image
				               				        	{
				               				        		url = "ScrumSprintMonitor.jpg"
				               				        	}
				               			}
				               	};
				this.rssFeed.channel.image.SetPubDate(SystemTime.Now());
				this.rssFeed.Save(this.rssFeedFilePath);

				string imagePath = Path.Combine(this.outputPath, this.rssFeed.channel.image.url);
				try
				{
					Resources.ScrumSprintMonitor.Save(imagePath);
				}
				catch (Exception ex)
				{
					this.logger.Warn(ex, "Found exception while trying to save {0}.", imagePath);

					throw;
				}
			}

			ReportableData = reportableData;
		}

		~FeedBuilderService()
		{
			Dispose(false);
		}

		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		private void Dispose(bool disposing)
		{
			if (disposing)
			{
				var connection = (ReportableData != null ? ReportableData.Connection : null);

				SuppressPropertyChanged = true;
				ReportableData = null;

				if (connection != null)
				{
					connection.Disconnect();
				}

				this.teamMemberInfoProvider = null;

				this.rssFeed = null;
			}
		}

		#region Implementation

		private IReportableData ReportableData
		{
			[DebuggerStepThrough]
			get { return this.reportableData; }
			set
			{
				if (value != this.reportableData)
				{
					this.reportableData = value;

					OnPropertyChanged("ReportableData");
				}
			}
		}

		private void SubscribeWorkItems()
		{
			UnsubscribeWorkItems();

			if (ReportableData == null || ReportableData.Connection == null)
			{
				return;
			}

			if (ReportableData.Connection.IsConnected)
			{
				var workItemStore = ReportableData.CurrentSprint.WorkItemStore;
				if (workItemStore != null)
				{
					this.workItemObserver = new WorkItemObserver(workItemStore, this.serviceProvider.GetService<ILogger<WorkItemObserver>>());
					this.workItemObserver.WorkItemsUpdated += OnWorkItemsUpdated;

					this.rssFeed.channel.title = ReportableData.CurrentSprint.Name;
					this.rssFeed.Save(this.rssFeedFilePath);
				}
			}
		}

		private void UnsubscribeWorkItems()
		{
			if (this.workItemObserver != null)
			{
				this.workItemObserver.WorkItemsUpdated -= OnWorkItemsUpdated;
				this.workItemObserver.Dispose();
				this.workItemObserver = null;
			}
		}

		private string GetTeamMemberDisplayAlias(string teamMemberName)
		{
			if (this.teamMemberInfoProvider == null)
			{
				return teamMemberName;
			}

			try
			{
				var teamMemberInfo = this.teamMemberInfoProvider.GetTeamMemberInformation(teamMemberName);

				if (teamMemberInfo != null)
				{
					if (!string.IsNullOrEmpty(teamMemberInfo.DisplayAlias))
					{
						return teamMemberInfo.DisplayAlias;
					}
					return teamMemberInfo.Name;
				}
			}
			catch (ArgumentException e)
			{
				this.logger.Warn(e, "Information for team member {0} not found.", teamMemberName);
			}

			return teamMemberName;
		}

		private static string GetWorkItemTypeName(IWorkItemAdapter workItemAdapter)
		{
			switch (workItemAdapter.Type)
			{
				case WorkItemType.Unknown:
					return "Work Item";
				case WorkItemType.UserStory:
					return "PBI";
				case WorkItemType.SprintBacklogItem:
					return "SBI";
				case WorkItemType.Defect:
					return "Bug";
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		#endregion

		#region Event Handlers

		private void OnWorkItemsUpdated(object sender, WorkItemsUpdatedEventArgs e)
		{
			try
			{
				var rssItemList = new List<Item>();

				foreach (var change in e.ChangesList)
				{
					switch (change.ChangeType)
					{
						case WorkItemChangeDescriptor.Operation.Added:
							OnWorkItemAdded(change.NewWorkItem, rssItemList);
							break;
						case WorkItemChangeDescriptor.Operation.Removed:
							OnWorkItemDeleted(change.OldWorkItem, rssItemList);
							break;
						case WorkItemChangeDescriptor.Operation.Changed:
							OnWorkItemChanged(change, rssItemList);
							break;
						default:
							throw new ArgumentOutOfRangeException();
					}
				}

				if (rssItemList.Count != 0)
				{
					const int upperLimit = 50;
					var rssItems = this.rssFeed.channel.items;
					while (rssItems.Count > 0 && rssItems.Count + rssItemList.Count > upperLimit)
					{
						rssItems.RemoveAt(0);
					}

					rssItemList.ForEach(x => this.rssFeed.channel.items.Add(x));
					this.rssFeed.Save(this.rssFeedFilePath);
				}
			}
			catch (Exception ex)
			{
				this.logger.Warn(ex, "Found exception while processing WorkItemsUpdated event.");

				throw;
			}
		}

		private void OnWorkItemAdded(IWorkItemAdapter workItem, ICollection<Item> rssItemList)
		{
			string description;
			if (string.IsNullOrEmpty(workItem.AssignedTo))
			{
				description = string.Format("{0} added {1} #{2} to the sprint, unassigned.", GetTeamMemberDisplayAlias(workItem.ChangedBy),
				                            GetWorkItemTypeName(workItem), workItem.Id);
			}
			else if (workItem.ChangedBy == workItem.AssignedTo)
			{
				description = string.Format("{0} added {1} #{2} to the sprint.", GetTeamMemberDisplayAlias(workItem.ChangedBy), GetWorkItemTypeName(workItem),
				                            workItem.Id);
			}
			else
			{
				description = string.Format("{0} added {1} #{2} to the sprint and assigned it to {3}.", GetTeamMemberDisplayAlias(workItem.ChangedBy),
				                            GetWorkItemTypeName(workItem), workItem.Id, GetTeamMemberDisplayAlias(workItem.AssignedTo));
			}

			var rssItem = new Item(description, GetWorkItemDisplayLink(workItem), HttpUtility.HtmlEncode(workItem.Title));
			rssItem.SetPubDate(workItem.ChangedDate);
			rssItemList.Add(rssItem);
		}

		private void OnWorkItemDeleted(IWorkItemAdapter workItem, ICollection<Item> rssItemList)
		{
			var rssItem =
				new Item(string.Format("{2} descoped {0} #{1}.", GetWorkItemTypeName(workItem), workItem.Id, GetTeamMemberDisplayAlias(workItem.ChangedBy)),
				         GetWorkItemDisplayLink(workItem), HttpUtility.HtmlEncode(workItem.Title));
			rssItem.SetPubDate(SystemTime.Now());
			rssItemList.Add(rssItem);
		}

		private string GetWorkItemDisplayLink(IWorkItemAdapter workItem)
		{
			return ReportableData.Connection.GetWorkItemDisplayUri(workItem).AbsoluteUri;
		}

		private void OnWorkItemChanged(WorkItemChangeDescriptor changeDescriptor, ICollection<Item> rssItemList)
		{
			var newWorkItem = changeDescriptor.NewWorkItem;
			var oldWorkItem = changeDescriptor.OldWorkItem;
			Item rssItem;
			var title = new StringBuilder();
			var description = new StringBuilder(HttpUtility.HtmlDecode(newWorkItem.Title));

			if ((changeDescriptor.ChangedFields & WorkItemChangeDescriptor.Field.State) == WorkItemChangeDescriptor.Field.State)
			{
				if (string.IsNullOrEmpty(newWorkItem.ChangedBy) || newWorkItem.ChangedBy == newWorkItem.AssignedTo)
				{
					switch (newWorkItem.State)
					{
						case WorkItemState.NotDone:
							title.AppendFormat("{0} reset {1} #{2} to Not Done.", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo), GetWorkItemTypeName(newWorkItem),
							                   newWorkItem.Id);
							break;
						case WorkItemState.InProgress:
							title.AppendFormat("{0} started working on {1} #{2}.", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo), GetWorkItemTypeName(newWorkItem),
							                   newWorkItem.Id);
							break;
						case WorkItemState.Done:
							title.AppendFormat("{0} completed {1} #{2}.", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo), GetWorkItemTypeName(newWorkItem),
							                   newWorkItem.Id);
							break;
						default:
							throw new ArgumentOutOfRangeException();
					}
				}
				else
				{
					switch (newWorkItem.State)
					{
						case WorkItemState.NotDone:
							title.AppendFormat("{3} reset {1} #{2} to Not Done (currently assigned to {0}).", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, GetTeamMemberDisplayAlias(newWorkItem.ChangedBy));
							break;
						case WorkItemState.InProgress:
							title.AppendFormat("{3} started working on {1} #{2} (currently assigned to {0}).", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, GetTeamMemberDisplayAlias(newWorkItem.ChangedBy));
							break;
						case WorkItemState.Done:
							title.AppendFormat("{3} finished {1} #{2} (currently assigned to {0}).", GetTeamMemberDisplayAlias(newWorkItem.AssignedTo),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, GetTeamMemberDisplayAlias(newWorkItem.ChangedBy));
							break;
						default:
							throw new ArgumentOutOfRangeException();
					}
				}
			}
			else
			{
				if ((changeDescriptor.ChangedFields & WorkItemChangeDescriptor.Field.AssignedTo) == WorkItemChangeDescriptor.Field.AssignedTo)
				{
					if (newWorkItem.AssignedTo == newWorkItem.ChangedBy)
					{
						title.AppendFormat("{0} took {1} #{2} from {3}.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy), GetWorkItemTypeName(newWorkItem),
						                   newWorkItem.Id, GetTeamMemberDisplayAlias(changeDescriptor.OldWorkItem.AssignedTo));
					}
					else if (newWorkItem.ChangedBy == changeDescriptor.OldWorkItem.AssignedTo)
					{
						title.AppendFormat("{0} reassigned {1} #{2} to {3}.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy), GetWorkItemTypeName(newWorkItem),
						                   newWorkItem.Id, GetTeamMemberDisplayAlias(newWorkItem.AssignedTo));
					}
					else if (newWorkItem.AssignedTo != changeDescriptor.OldWorkItem.AssignedTo)
					{
						title.AppendFormat("{0} reassigned {1} #{2} from {3} to {4}.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy),
						                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, GetTeamMemberDisplayAlias(changeDescriptor.OldWorkItem.AssignedTo),
						                   GetTeamMemberDisplayAlias(newWorkItem.AssignedTo));
					}
				}

				if ((changeDescriptor.ChangedFields & (WorkItemChangeDescriptor.Field.RemainingWork | WorkItemChangeDescriptor.Field.DoneWork)) != 0)
				{
					if (title.Length != 0)
					{
						title.AppendLine();
					}

					switch (changeDescriptor.ChangedFields & (WorkItemChangeDescriptor.Field.RemainingWork | WorkItemChangeDescriptor.Field.DoneWork))
					{
						case WorkItemChangeDescriptor.Field.DoneWork:
							title.AppendFormat("{0} updated done work on {1} #{2} to {3} hours.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, newWorkItem.DoneWork);
							if (!double.IsNaN(oldWorkItem.DoneWork))
							{
								if (description.Length > 0)
								{
									description.AppendLine();
								}
								description.AppendFormat("Previous value: {0} hours done.", oldWorkItem.DoneWork);
							}
							break;
						case WorkItemChangeDescriptor.Field.RemainingWork:
							title.AppendFormat("{0} updated remaining work on {1} #{2} to {3} hours.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, newWorkItem.RemainingWork);
							if (!double.IsNaN(oldWorkItem.RemainingWork))
							{
								if (description.Length > 0)
								{
									description.AppendLine();
								}
								description.AppendFormat("Previous value: {0} hours remaining.", oldWorkItem.RemainingWork);
							}
							break;
						default:
							title.AppendFormat("{0} updated {1} #{2} to {3} hours done and {4} hours remaining.", GetTeamMemberDisplayAlias(newWorkItem.ChangedBy),
							                   GetWorkItemTypeName(newWorkItem), newWorkItem.Id, newWorkItem.DoneWork, newWorkItem.RemainingWork);
							if (!double.IsNaN(oldWorkItem.DoneWork) && !double.IsNaN(oldWorkItem.RemainingWork))
							{
								if (description.Length > 0)
								{
									description.AppendLine();
								}
								description.AppendFormat("Previous values: {0} hours done and {1} hours remaining.", oldWorkItem.DoneWork, oldWorkItem.RemainingWork);
							}
							break;
					}
				}
			}

			if (title.Length != 0)
			{
				rssItem = new Item(title.ToString(), GetWorkItemDisplayLink(newWorkItem), HttpUtility.HtmlEncode(description.ToString()));
				rssItem.SetPubDate(newWorkItem.ChangedDate);
				rssItemList.Add(rssItem);
			}
		}

		#endregion
	}
}